import sys
import codecs
import re
# simple vCard parser, 
# for version 3, http://tools.ietf.org/html/rfc2426

# license: GPL 2 or later
# author: Pavel Nemec <pnemec@suse.cz>
# help: Daniel Dotsenko <dd@accentsolution.com>
# 2007-06-29 HACK WEEK
'''
How to use this library:

Purpose:
    It is designed for ease of data reading. Hence data storage is organized around ease of reading and searching.
    We use exactly the same keys used in vcard for filed names:
    FN, N, NICKNAME, PHOTO, BDAY, ADR, LABEL, TEL, EMAIL,
    MAILER, TZ, GEO, TITLE, ROLE, LOGO, AGENT, ORG, CATEGORIES, NOTE,
    PRODID, REV, SORT-STRING, SOUND, URL, UID, VERSION, CLASS, KEY
    We do NOT convert the strings assigned to the keys, YOU will do it in your code.
    But, we do provide some helper functions to find some "best match" items less monotonous.
    see contactClass.name()

Initialization:
    import readvcard # this is this module
    allcontacts = readvcard.init() # this uses custom module to detect and load default KDE vcard files. See init() for manual set up.
    # allcontacts is a collection of class instances simulating a 3-dimensional array
    # allcontacts,  *behaves* as a list type. Returns []
    #  '- contact, *behaves* as a dict type. Returns {} where
    #    '- key / vcard field name. Ex: 'TEL', 'N' , *behaves* as a dict type. Returns {}
    #      '- location type. Ex: 'HOME', a string behaving as a key in a dict type. Returns string/unicode
    #      '- actual data , is always a unicode or string type.
    # ex: string telnum = allcontacts[0]['TEL']['HOME']

Reading Data:
    contact = allcontacts[0] # allcontacts *behaves* as a list of contacts.
    variations_of_telefone = contact['TEL']
    name = variations_of_telefone['ANY'] # also sometimes available ['DEFAULT', 'HOME', 'WORK' etc]
                                    # 'default' is our custom type. We use it when vcard entry does not specify a type like 'home', or 'work'
    # you can sqeeze all that together into
    contact['TEL']['HOME'] # which is approximately equivalent to
    contact['TEL']('HOME') # or
    contact['TEL']('ANY') # which is equivalent to
    contact['TEL']() # whis is equivalent to
    contact('TEL')
    contact() # just gives this contact's UID, if was present in vcard. You can pass that to kaddressbook and get that person's card there.
    # Note, [] does a strict look up and returns an actual object, which may be an instance of dataUnitClass
    # or '' empty string if not found. () does "best match" lookup and always returns a string.
    # ANY is a special keyword. It is not used in the DB as an entry type,
    # it just mean "get any available", which may not be the best choice for you
    # in case when we look up phone numbers
    # Now, if you want to make your life easier in cases when you don't care where the data is taken from
    # like in case you need any "name" and will accept strings from "FN","NICKNAME","ORG" etc
    # you can pass the whole list like this:
    contact(['NICKNAME','FN','ORG']) # or just use
    contact.name() # which is a special helper function that looks in ['N','FN','NICKNAME','TITLE','ORG']
    contact.formatAddress() # is another helper. Both can take additional arguments. See remarks for each of the fn's

Writing data:
    # This lib does NOT write to file. We only support changing in-memory db.
    # use contactClass.set() method to change items. See comments in the function for arguments.

Searching data:
    # To search data in a groupClass instance:
    found = contacts.find('john', keys=['N','ORG']) # or
    found = contacts.find('John',['N','ORG']) #or
    found = contacts.find('John') # in which case the search is done in ['N','FN','EMAIL','NICKNAME','TITLE','ORG','TEL']
    # returning value is another groupClass instance populated by contact instances with fields matching the search string.
    # you can search that, new instance in the same way for some other string.

    # to search iside of individual contactClass instance for occurances of text, use the same approach:
    found = contact.find('john', keys=['N','ORG']) # or
    found = contact.find('John',['N','ORG']) #or
    found = contact.find('John') # in which case the search is done in ['N','FN','EMAIL','NICKNAME','TITLE','ORG','TEL']
    # but in this case the returning value is a dict type where:
    # - keys are name of field ("TEL","N") where the string was found.
    # - values are datatype name ("HOME","DEFAULT")
    # You can also search inside of dataUnitClass instance, but why would you?
'''


class dataUnitClass:
    '''
    NOTE: This memo is a collection of crap that accumulated over several versions. Some of this may not be correct.

    This is the class assigned to the keys in contact's card.
    Example:
    Jack's Card has a business and home phone.
    Jack's card object (a simple dict type):
    - 'PHONE' key (an instance of this class)
      - methods of this class that deliver the data.

    Why making it a class? Keys like PHONE can be of many types
    keeping them all in list or dict structure complicates retrieval
    for cases when we need "default" "short" values or need special handling.
    For example, adding a new "home" type of "phone" requires making sure that
    "default" number will now return the updated data. 
    On the other hand, polling for "home","work","spaceship","whatever that is available"
    numbers by the client program directly is very annoyng and monotonous.

    Using the dict approach like so: clinetcard['PHONE'] will give you a pointer to instance of this class.

    There are 2 ways to get the data out of this class: dictionary lookup and function call.
    Calling clinetcard['PHONE']() ( or clinetcard['PHONE'].get() ) will poll all awailable
    types and will give you a NEW COPY of best match from ['HOME','WORK' whatever awailable]
    or an empty string.
    Using dictionary like so clinetcard['PHONE']['HOME'] 
    will be a strict lookup and give you a string with data or empty.

    Again, this is not a Contact level class, but a contact's key/data field class.
    '''

    def __init__(self, data=None, datatype='DEFAULT'):
        self.db = {} # this is the main data storage... all values are strings/unicode
        if data:
            self.set(data=data, datatype=datatype)


    def __getitem__(self, item):
        '''
        This enables calling this object in a dictionary style, like so: classInstance['WORK']
        '''
        try:
            return self.db[item]
        except:
            return ''

    def __len__(self):
        return len(self.db)

    # the next 2 functions make this method iterable by
    # returning consecutive value from self.db
    def __iter__(self):
        self.iterobj = self.db.iterkeys()
        return self
    def next(self):
        try:
            return self.iterobj.next()
        except StopIteration:
            raise StopIteration

    def index(self):
        return self.db.keys()
    def keys(self):
        return self.db.keys()

    def set(self, data=None, datatype='DEFAULT'):
        '''
        This is also the "add" function. It allows setting as well as setting "empty" data to entries.
        Not passing the data, removes the bit.
        - datatype is a type of phone/address/email. Ex: "WORK", "HOME"
          It is possible that the key does not support types.
          In that case, we use "default" as type.
        - data is a whatever you decide it to be... string, list, dict.
          the getter function has to be intelligent enough to parse that properly,
          so make sure the decoder for that type is present in get() and string()
          * The data storage format is Work-in-progres. May change later to support complex Image data types
        '''
        if data: # presence of data sets it.
            self.db[datatype] = data
        else: # absence of data removes the data type.
            try:
                del db[datatype]
            except:
                pass

    def __call__(self, datatype='ANY'): return self.get(datatype)
    # it's just a shortcut. So instead of getting a best-match data
    # by valling thisObject.get(), we can just thisObject()

    def get(self, datatype='ANY'):
        '''
        - datatype is a type of phone/address/email. Ex: "WORK", "HOME", "DEFAULT" 
          ("DEFAULT" is our custom type for entries that don't have types.)
          It is possible that the key does not support types, you don't have to specify it.
          In that case, we use "DEFAULT" as type internally.

          NOTE, datatype is activated with "ANY" as argument. This may result in
          almost random returned value if there are many choices.
          Hand-specify datatype argument if you want strict lookups.
        '''
        # I am not sure why I put this is in... probably to accommodate lazy lookups.
        # this will try to return something alternative if the requested type is unavailable.
        if datatype=='ANY' and len(self.db):
            if self.db.has_key('DEFAULT'):
                datatype = 'DEFAULT'
            else:
                datatype = self.db.keys()[0] # don't know how to choose, lets just pick the 1st one.
        try:
            # note, "ANY" will always fail here.
            data = self.db[datatype]
            # at this time we will just assume that it's a string. Add processing later.
        except:
            data = '' # i should really make it None, but this will cause headaches for lib users
        return data

    def types(self):
        return self.db.keys()

    def rawdata(self):
        '''
        important! This function returns a COPY of the data NOT the POINTER to it.
        Use this for hand pilfering and manipulation without fear of damaging the data.
        '''
        return self.db.copy()

    def __findRegEx(self, i, text):
        return bool(text.search(i))
    def __findString(self, i, text):
        return bool(i.find(text) != -1)

    def find(self, text, **kw):
        '''
        Use for quick EXACT string searching through the data key DB.
        Returns type names for type values that contain the text.

        This function assumes that all values are strings
        Will brake if other data types are used.

        Note, this will not work well for looking up FORMATTED phone numbers.
        Only contiguous, uninterrupted by formatting numerical blocks 
        will be matched.

        If you need a more lax way of searching for numbers, use get() for data,
        clean out formatting and parse by hand.

        fuzzy = [True/False] - one optional argument that allows matches for
         similar-looking characters, line E with umlauts, asterisks etc.
         it is much slower, so use with caution.
        '''
        try:
            fuzzy = kw['fuzzy']
            # note, the search word is expected to be a COMPILED!!!! reg ex object.
        except:
            fuzzy = False
            # search word is a normal string/unicode

        if fuzzy:
            schfn = self.__findRegEx
        else:
            schfn = self.__findString
            text=text.upper()

        return [i for i in self.db.keys() if schfn(self.db[i].upper(),text)]

class contactClass:
    def __init__(self):
        self.db = {}

    def __len__(self):
        return len(self.db)

    # the next 2 functions make this method iterable by
    # returning consecutive value from self.db
    def __iter__(self):
        self.iterobj = self.db.iterkeys() # note! we are iterating through KEYS only.
        return self
    def next(self):
        try:
            return self.iterobj.next()
        except StopIteration:
            raise StopIteration

    def index(self):
        return self.db.keys()
    def keys(self):
        return self.db.keys()

    def set(self, key, data, datatype='DEFAULT'):
        '''
        ex:
        key = TEL
        data = 1-345-555-1212
        datatype = WORK
        '''
        if not self.db.has_key(key):
            self.db.update({key:dataUnitClass(data=data, datatype=datatype)})
        else:
            self.db[key].set(data=data, datatype=datatype)

    # the following 3 get functions return the contents, but in slightly diffrent way
    def __getitem__(self, item):
        '''
        This enables calling this object in a dictionary style,
        like so: classInstance['WORK']

        This returns a POINTER to the contents. Be very careful with it.
        Try to only read from it, or manipulate (sort, change) only if 
        you want the changes to affect the actual database.

        Again, we return a Live Pointer to the data.
        '''
        try:
            return self.db[item]
        except:
            return dataUnitClass()

    def __call__(self, key='UID', datatype='ANY'):
        return self.get(key, datatype)
    # it's just a shortcut. So instead of getting a best-match data
    # by calling thisObject.get(), we can just thisObject()

    def get(self, keys, datatype='ANY'):
        '''
        This is used to retrieve data for the contacts.
        Note, this function is lax on arguments and tries to 
        guess the omitted arguments.

        If datatype is not passed, it is "ANY" by default.
        This means if "DEFAULT" is present, give that, otherwise any other awailable

        If anything else is passed as datatype and not found, or nothing at all found
        do strict lookup and return empty object.
        ex:
        keys = "PHONE"
        datatype = "WORK"
        Returns:
        data = "1-345-555-1212"

        Now, to make things easier for programmers there, you can pass a list of keys
        to this function to make it return the 1st field that actually contains something.
        Note, N will always contain at least ';;;;;' and will always be non empty.
        so, you can pass the keys as:  keys = ['FN','ORG'] to ensure that at least some name will come out.
        '''
        data = '' # by default, our db in this class is {}, where keys are strings
        if type(keys)!=type([]):
            keys = [keys]
        while (not data) and keys:
            key = keys.pop(0)
            if self.db.has_key(key):
                try:
                    data = self.db[key].get(datatype=datatype)
                except:
                    pass
        return data

    def find(self,text,**kw):
        '''
        By default, this fn searches in name/organization type of fields.

        fuzzy = [True|False] - see parent class's find() for info.

        keys=['key1','key2'] - if passed to the function, we use that.
         Specifying keys is usefull when searching for things like UID or
         making the search faster by narrowing the scope.
         EX: contact.find('John',keys=['N','ORG'])
          or just   contact.find('John')

        Searched are always case INsensitive! (for explanation see dataUnitClass.find())
        '''
        try:
            fuzzy = kw['fuzzy']
            # note, if True, the search word will be made into a COMPILED!!!! reg ex object.
        except:
            fuzzy = False
            # search word is a normal string/unicode

        if fuzzy:
            if type(text) in [type(''),type(u'')]: # if this is passed from groupClass, it was already compiled, else, it's text
                text = text.upper() # if searching by simle string, dataUnitClass takes care of UPPER
                for letter in self.fuzzychars:
                    text = text.replace(letter, self.fuzzychars[letter])
                #print "searching for ", text
                text = re.compile(text) # compiled objects are faster to search for.

        keys = self.db.keys()
        if kw.has_key('keys'):
            keys = list(set(keys).intersection(set(kw['keys'])))
        else:
            keys = list(set(keys).intersection(set(['N','FN','EMAIL','NICKNAME','TITLE','ORG','TEL'])))

        found = {}
        for key in keys:
            a = self.db[key].find(text,**kw)
            # a is a [] with list of datatypes with text found in data
            # so, you can use it as an address to get the data.
            if a:
                found[key]=a
        return found

    def rawdata(self):
        '''
        important! This function returns a COPY of the data NOT the POINTER to it.
        USe this for hand pilfering and manipulation without fear of damaging the data.
        '''
        return self.db.copy()

    ################
    # Helper functions.
    def name(self, fields=None):
        # looks at these fields and returns 1st non-empty entry
        # ['N','FN','NICKNAME','TITLE','ORG']
        name = ''
        if not fields: # putting it in fn declaration uses up the list, so I have to do it here.
            fields = ['FN','N','NICKNAME','TITLE','ORG']
        while (not name) and fields:
            field = fields.pop(0)
            if self.db.has_key(field):
                if field == 'N':
                    # Family Name, Given Name, Additional Names, Honorific Prefixes, and Honorific Suffixes
                    parts = self.db['N']().split(';')
                    try:
                        name = (' '.join([i for i in [parts[3],parts[1],parts[2],parts[0],parts[4]] if i])).strip()
                    except:
                        name = ''
                else:
                    name = self.db[field]()
        return name

    def formatAddress(self, datatype='ANY',template=None):
        '''
        Turns ADR field data into formatted, multiline string.
        datatype - one of "HOME","WORK","DEFAULT" etc
         by default it is "ANY" which grabs any available
        template - tagged, preformatted string
         default template (USA-style) = '$POBOX\n$STREETADR\n$EXTADR\n$CITY, $REGION $CODE\n$COUNTRY'
        '''
        # ADR list order:
        # post_office_box; extended_address; street_address; locality; region; postal_code; country
        adr = self['ADR'](datatype)
        if len(adr)==6: # domestic address
            adr.append('United States of America')
        if not template:
            template = '$POBOX\n$STREETADR\n$EXTADR\n$CITY, $REGION $CODE\n$COUNTRY'
        if adr:
            adr = adr.replace('\\n','\n').replace('\,',',').replace('\;',':').split(';') # note, I am chitting there with ;>:
            for tag, data in zip(['$POBOX','$EXTADR','$STREETADR','$CITY', '$REGION', '$CODE','$COUNTRY'],adr):
                template = template.replace(tag, data)
            # taking out empty lines.
            adr = '\n'.join([i for i in template.splitlines() if i])
        return adr

class groupClass:
    def __init__(self):
        self.db =[] # this is the main, parsed list of contacts.

    def __getitem__(self, item):
        '''
        This enables calling this object in a dictionary style, like so: classInstance['WORK']
        '''
        try:
            return self.db[item]
        except:
            return contactClass() # empty

    def __len__(self):
        return len(self.db)

    # the next 2 functions make this method iterable by
    # returning consecutive value from self.db
    def __iter__(self):
        self.nextiterval = 0
        return self
    def next(self):
        if len(self.db)-self.nextiterval < 1: # if this is the end of data list.
            raise StopIteration
        else:
            a = self.db[self.nextiterval]
            self.nextiterval +=1
            return a

    def append(self, contact):
        self.db.append(contact)

    def remove(self, contact):
        try: self.db.remove(contact)
        except: pass

    # used for fuzzy search. refer to as self.fuzzychars
    fuzzychars = {
        'A': u'[A|\u0226|\u0202|\u0200|\u0104|\u0102|\u0100|\xc5|\xc4|\xc3|\xc2|\xc1|\xc0|\u01cd|\u0410]',
        'C': u'[C|\xc7|\u0106|\u0108|\u010a|\u010c]',
        'B': u'[B|\u0411]',
        'E': u'[E|\u0228|\u0206|\u0204|\u018e|\u0112|\u0114|\u0116|\u0118|\u011a|\xc8|\xc9|\xca|\xcb|\u0415]',
        'D': u'[D|\xd0|\u010e|\u0110|\u0414]',
        'G': u'[G|\u011c|\u011e|\u0120|\u0122|\u0193|\u01e6|\u01f4|\u0413]',
        'I': u'[I|\xcc|\xcd|\xce|\xcf|\u0128|\u012a|\u012c|\u012e|\u0130|\u0208|\u020a|\u0418]',
        'H': u'[H|\u021e|\u0126|\u0124|\u0425]',
        'K': u'[K|\u0136|\u0198|\u01e8|\u041a]',
        'J': u'[J|\u0134|\u0416]',
        'L': u'[L|\u0139|\u013b|\u013d|\u013f|\u0141|\u041b]',
        'O': u'[O|\xd2|\xd3|\xd4|\xd5|\xd6|\xd8|\u014c|\u014e|\u0150|\u01ea|\u01ec|\u01fe|\u020c|\u020e|\u022e|\u041e]',
        'N': u'[N|\xd1|\u0143|\u0145|\u0147|\u014a|\u019d|\u01f8|\u041d]',
        'S': u'[S|\u0218|\u0160|\u015e|\u015c|\u015a|\u0421]',
        'R': u'[R|\u0154|\u0156|\u0158|\u0210|\u0212|\u0420]',
        'U': u'[U|\xd9|\xda|\xdb|\xdc|\u0168|\u016a|\u016c|\u016e|\u0170|\u0172|\u01d3|\u01d7|\u01d9|\u01db|\u0214|\u0216|\u0423|\u042e]',
        'T': u'[T|\u021a|\u0166|\u0164|\u0162|\u0422]',
        'W': u'[W|\u0174|\u0416]',
        'V': u'[V|\u0412]',
        'Y': u'[Y|\u0232|\u01b3|\u0178|\u0176|\xdf|\xdd|\u0419]',
        'Z': u'[Z|\u0224|\u01ee|\u017d|\u017b|\u0179|\u0417]',
        }

    def find(self, text, **kw): # **kw is for named-argument pass through.
        '''
        Searches recursively through the whole contacts database.
        returns a collection of addresses to the data. See the functions 
        for contactClass.find() and dataUnitClass.find() for explanation of 
        individual returns. Return of this function looks like this.
        {<contactClass instance at 0xb7c7c6ec>: {'N': ['DEFAULT']}}
        where, you can retrieve the exact found data by calling
         <contactClass instance at 0xb7c7c6ec>['N']['DEFAULT']

        fuzzy = [True|False] - activates matching of "similarly-looking/sounding".characters. False by default.
        keys = [] list of data fields in which to search the string. See contactClass.find() for defaults
        '''
        try:
            fuzzy = kw['fuzzy']
            # note, if True, the search word will be made into a COMPILED!!!! reg ex object.
        except:
            fuzzy = False
            # search word is a normal string/unicode

        if fuzzy:
            text = text.upper() # if searching by simle string, dataUnitClass takes care of UPPER
            for letter in self.fuzzychars:
                text = text.replace(letter, self.fuzzychars[letter])
            #print "searching for ", text
            text = re.compile(text) # compiled objects are faster to search for.

        found=groupClass()
        for i in self.db:
            # a is a dict type and is a result of contactClass.find()
            if i.find(text, **kw):
                found.append(i)
        return found

    def sort(self, fields=None):
        '''
        fields - optional list type argument indicating in which field to look
         when pulling sortable string. It is a list because sometimes
         one field is not applicable to all contacts, i.e. "FN" will be empty for some "ORG"
         This way you can pass fields=['FN','ORG']
         If nothing is passed to fields, we use output from .name() which
         checks many name-like fields and should provide some sortable strings.
        '''
        # at this time, we only sort by one field. Later, maybe, implement multifield sorting.
        if fields:
            twinlist = [(cont(fields).upper(),cont) for cont in self.db]
        else:
            twinlist = [(cont.name().upper(),cont) for cont in self.db]
        twinlist.sort()
        self.db = [i[1] for i in twinlist]
        del twinlist

    def printall(self):
        '''
        Used for testing. Prints the whole contact inventory of this groupClass instance.
        '''
        for contact in self.db:
            print "============================="
            for key in contact.index():
                print ' '*(15-len(key))+key, ' = ', contact[key]()

def parseFiles(files, groupobj=None):
    '''
    This function will go through every line in evey file given in self.files and
    will simply match/convert the vcard standart keys to a list of dict objects mirroring
    the contents of the original file.
    Note, we do not do string-level MATCHES for keywords. We just take whatever the
    key line is and make a dictionary key out of it.
    Each tag/key will be a another dictionary, that will at a minimum contain a "value" key.
    The result of key parsing is:
    contactInstance{'TYPE':'the data value', 'ANOTHERTYPE':'the data'}
    '''
    if not groupobj:
        groupobj = groupClass()

    # although I like this approach from the point of view of minimum memory usage,
    # i do not like it due to it's reliance on reading actual files.
    # for situations when vcards are pulled from net, or other non-local sources,
    # we need to have a way to parse normal string and list objects.
    # think about file-like object processing.
    for file in files:
        try:
            fr = codecs.open(file,'rb', encoding='utf-8')
        except IOError:
            print 'Error:  Couldn\'t parse file: .', file
            continue
        # looked at the spec and realized that doing the parsing line-by-line may result is big problems
        # entries and whole vcards could be aggregated on one line.
        # line-by-line also breaks PHOTO, LOGO, and NOTES reading.
        # what's worse, vcards could be NESTED! Holly crap, demn spec!
        # what we will do now - look for 1st "BEGIN:VCARD"
        # and pass card processing to a cardParser.
        # It will work untill it finds a closing/matching END:VCARD
        # it will call itself if a nested BEGIN is found
        # (better yet, for now, we will just intelligently ignore the nested ones)
        line=fr.readline()
        while line: # note, \n\r is still there on empty lines. This checks for end of file.
            start = line.find("BEGIN:VCARD")
            if  start != -1:
                #PARSING NEW VCARD, will watch for closing END but skip nested BEGIN-END pairs.
                # in strange cases when there is some crap in front of BEGIN, we will cut it off.
                c, line = __parseCard(line[start+11:], fr)
                # returns a contact class with data and
                # a remainder of a line read from file, but which does not belong to the card.
                # or, if the line was empty, reads new line.
                c.set('FILENAME', file)
                groupobj.append(c)
            else:
                # this takes care of empty lines between cards.
                line = fr.readline()
    return groupobj

def __parseCard(line, fileobj):
    # will read from given file object
    # need to find a closing END:, skipping nested BEGIN-END
    # also checking each line for multible tags.
    # line is a string with some text that parent looked at and decided that
    # it is a start of this vcard.
    tagcounter = 1
    card = ''
    cardend = 0
    unsupported = [
        'PHOTO', # these two take too much memory, no easy way to load it into our SuperKaramba
        'LOGO', # so, may need to save these to disk to load = too much trouble at this time.
        'VERSION', # we expect v3 and nothing else.
        'AGENT', # this one has nested vcards. I don't think Kaddressbook writes these. Either way, too much touble to support nesting at this time.
        'BEGIN',
        'END',
        'NOTE', # note, we are UNsupporting the Note ONLY to save memory. Not a technical issue.
        'REV', # i think this one is "last revision" and I cant figure out why we would need it.
        '', # this is here to bypas glitches in key-reading system.
        ]
    # we will run next loop until we find closing END or reach end of file.
    # cardend will be 0 when matching, closing END is found
    # we collect ALL lines between the initial BEGIN and matchin, closing END
    # this includes NESTED BEGIN / END.
    # we collect the contents of the card into a multi-line (list) var 'card'
    # once we find the end of the card, we
    # 1. note the place where we ended reading
    # 2. parse the 'card' and assign the results to a contactClass instance,
    # 3. pass the instance, the unread/remaining characters on 'line' var, and the file object back to parent.
    #    where the contact will be put into a contactS db, and we are called again until the end of file.
    while tagcounter > 0 and line:
        tags = re.findall('BEGIN:VCARD|END:VCARD', line)
        if not tags:
            card = card + line
            line = fileobj.readline()
        else:
            # many tags are on the same line. Looking for 1st.
            i = re.finditer('BEGIN:VCARD|END:VCARD', line)
            tag = i.next()
            while tagcounter > 0 and tag:
                if tag.group() == 'BEGIN:VCARD':
                    tagcounter +=1
                else: # END:VCARD
                    tagcounter -=1
                    cardend = tag.start()
                try:
                    tag = i.next()
                except:
                    tag = None
            if tagcounter == 0: # yey! we found our end.
                card = card + line[:cardend]
                line = line[cardend+9:] # we are throwing out the closing tag
            else:
                card = card + line
                line = fileobj.readline()
    # at this stage, we have the following important pieces:
    # card (string with our card), line (remaining stuff), fileobj
    # we need to clean up the string. The spec allows some line folding, so we unfold.
    #print "the card before ", card
    card = [i for i in card.replace('\r\n ','').split('\r\n') if i]
    #print "the card after ", card
    # Bonus: that line will also put all of binary data on the one line next to the key.
    contact = contactClass()
    nesting = 0
    #for ca in card: # for testing.
        #print "=:%s:=" % ca
    for li in card:
        # PROCESSING EACH LINE IN THE CARD NOW.
        # li is a "line" in the card. The reason it is in "" is it's not a line
        # from original vCard source but a single keyword + ALL of the data per line.
        # This means if keyword is PHOTO, all of the folded lines of data were already unfolded into
        # this one long line.
        # all other keywords (like in case of AGENT nesting) were unfolded into separate consecutive lines.
        # so, what you have now in every "line" is a Guarantee (if bugs do not show up) that ALL of the data
        # is on the same line as the single keyword.
        # note, starting / closing BEGIN and END were already stripped out.
        # if they are found now, they can only be nested.
        # this routine will take apart the line into 3 distinct pieces.
        # key (ex: PHONE), datatype (ex: HOME) and data (ex: "1-555-555-1212")
        # and will assign it to contact's db of entries.
        keylist, data = li.strip().split(':',1) # splitting the line on 1st : into keys and value
        keylist = keylist.split(';') # key field can be a composite of key and type entries.
        # setting KEY
        key = keylist.pop(0).upper()
        key = key.rsplit('.',1)[-1] # removing "grouping" prefix allowed by vCard spec
        if key == 'BEGIN': nesting +=1
        if key == 'END': nesting -=1
        if (not nesting) and (not key in unsupported):
            # we cannot (do not want to) handle nested vcards yet. Gets here only if level of nesting=0
            data = data.strip() # removing spaces and \n\r from both ends.
            # setting Datatype
            datatypes = ['DEFAULT']
            if keylist: # i.e. it's not empty
                # we have type and other key options.
                # at this time, we do not know how to hadle complex
                # objects, like PHOTO... so we will just pass on that.
                # they usually have more than one qualifyer in key field.
                # now looking for "TYPE" key values. ex: out of the "line"
                # ADR;TYPE=dom,home,postal,parcel;OTHERTYPE=blahblahblah:;;123 Main Street;Any Town;CA;91921-1234
                # at this stage we have list with ['TYPE=dom,home,postal,parcel','OTHERTYPE=blahblahblah']
                try:
                    # what happens next:
                    # keylist item is split at "=", if left part == 'Type' keep right part
                    # the right part is split at ',' and passed as a LIST of datatypes.
                    datatypes = [li.split('=')[1].upper() for li in keylist if (li.split('=')[0] in ['TYPE','type'])][0].strip().upper().split(',')
                    # note, datatype is always a list, even if only one item, or no items at all found.
                    # in our example, datatype = ['dom','home','postal','parcel']
                    trash = list(set(datatypes).intersection(set(['DOM','INTL','PARCEL','POSTAL'])))
                    for i in trash:
                        datatype.remove(i) # we will treat all addresses as international.
                except:
                    pass
            # now we have: string key, string data, list datatype and are ready to put them into our db
            # note, our "line" variable is still hanging around and contains the remains of chars hanging after END:VCARD for current card.
            for datatype in datatypes:
                # same address may be assigned to many types, like HOME, WORK, OTHER.
                # we will deposit the same data in 3 different places.
                # it may be a waste of memory, but easier that implementing a relational database.
                contact.set(key, data, datatype)
    # line contains remains of data on last read line, that was not used as part of this vcard.
    # They may have parts of next vcard, or, nothing at all.
    return contact, line

class cachedSearch:
	'''
	Really a hack. Trying to save search time at the expense of using some memory and
	storing some extra pointer-collection bundles. The bonus is, we can serch inside of
	the stored bundles.

	How it works:
	Every time a request for a search comes in, we look in the cache indexed by the search string.
	If nothing found, it does the search and returns the outcome.

	How to use it:
	addressbook = cachedSearch(list of vcard files)
	addressbook.addfiles(list of additional files)
	searchresult = addressbook('john')
	'''
	db = {}
	def __init__(self, filelist):
		self.db[''] = parseFiles(filelist)
		self.db[''].sort()
		# note, our main addressbook will sit in self.db['']
		# this logically represents a search for a string of '' - i.e. no limitation = give me everything.
		# if you want to get the whole book, you can do book = self.db[''] and
		# steal this item directly from the instance if you want
		# or, you can use one of __call__ or __getitem__ with empty string to get it.
		# I suggest the call, since it does not require any arguments.
	def addfiles(self, filelist):
		'''
		This is how you add info to the self.db master database.
		Note, adding files automatically clears out the cache.
		'''
		self.db = {'':parseFiles(filelist, self.db[''])}
		self.db[''].sort()
	def __call__(self, item=''):
		'''
		This is where all the magic happens. If nothing was passed, the whole address book
		(the groupClass instance with the whole addressbook) is returned.
		If something is passed, we look up in the db if it was searched for already.
		If it was, there is a cache of the groupClass object in the db, which we return.
		If not, we do the actual search, save it in cache and then, return that.
		We return an instance of groupClass, with the subset of contacts matching the search.
		'''
		if item:
			item = item.upper()
			for i in reversed(range(0,len(item)+1)):
				# we get a list of strings with gragually diminishing tails.
				if self.db.has_key(item[:i]):
					break
			if item[:i]!=item: # if the search string does not have any cached results in the db,
				self.db[item] = self.db[item[:i]].find(item, fuzzy=True) # note, if i = 0, the string is '', which refers to the whole addressbook
		return self.db[item]
	def __getitem__(self, item):
		'''
		This is a shortcut that allows you to use this method instance as a dictionary.
		It behaves exactly like __call__ but, you WILL HAVE to pass '' as argument if
		you want to get the whole addressbook, and, of course, you will use the [] instead of ()
		'''
		return self.__call__(item)

# The following are tester functions.
def initFromKDE():
    '''
    Helper function. Only works in KDE environment and with 
    uservcards module present.
    Pull up all registered vCards into DB and returns 
    contacts object.
    '''
    import uservcards
    rvc = parseFiles(uservcards.getUserFiles())
    # what we got is an instance of groupClass populated as a list of instances of contactClass
    # note, we could pass an existing instance of groupClass to the function.
    # and would get same instance back, but with all new entries added.
    return rvc
def test():
    all = initFromKDE()
    for contact in all:
        print "============================="
        for key in contact.index():
            # key is "NAME", "TEL", "EMAIL" etc.
            # note, each key may have subtypes, like "HOME", "WORK"
            # which you can get directly by contact[key][subtype]
            # but we will ask the key method to look up most appropriate
            # value by itself, by calling the key object as a function - ()
            print ' '*(15-len(key))+key, ' = ', contact[key]()
            #print 'email is: ',contact['EMAIL']()
            #print 'email is: ',contact['EMAIL']['WORK']
            # almost same as    contact['EMAIL']('WORK')
            # both return an empty string if "WORK" is not found.
